Writing functions in R

A community-driven R package

Function basics

What is a function

takes input –> does something –> returns output

mean(c(1, 2, 3))
[1] 2


A function needs a name, arguments in (), and a body in {}

subtract <- function(arg1, arg2) { 
  arg1 - arg2 
} 


subtract(2, 1)
[1] 1

Why do we need functions

  • Readability
  • Organisation
  • Modularity
  • Reusability

Imagine calculating the mean without standard functions like mean or sum:

  data <- c(1,2,3)
  total <- 0
  count <- 0
  for (value in data) {
    total <- total + value
    count <- count + 1
  }
  total/count
[1] 2

Arguments

Arguments need to be provided in the correct order, or specified by name:

subtract(2, 1)
[1] 1


subtract(1, 2)
[1] -1


subtract(arg2 = 1, arg1 = 2)
[1] 1

Default values

Make function use more convenient, can hide complexities.

subtract(2)
Error in subtract(2): argument "arg2" is missing, with no default


This will work if we set a default for arg2:

subtract <- function(arg1, arg2 = 1) {
  arg1 - arg2
}


subtract(2)
[1] 1


Ellipsis (‘…’)

Additional, optional arguments can be allowed by using ‘…’ as the last argument:

my_plot <- function(arg1, arg2, ...) {
  plot(arg1, arg2, ...)
}


my_plot(2, 1, col = "red", pch = 17, cex = 2)

Return

A function generally should return something, but this does not:

subtract <- function(arg1, arg2) {
  result <- arg1 - arg2
}
subtract(2,1)


Return explicitly with return, or place return value at the end of the function:

subtract <- function(arg1, arg2) {
  result <- arg1 - arg2
  return(result)
}
subtract(2,1)
[1] 1


Return multiple objects

just_return <- function(arg1, arg2) {
return(arg1)
return(arg2)
}
just_return(2, 1)
[1] 2

This did not work as intended. R functions only return one object. Instead use lists or other data structures:

just_return <- function(arg1, arg2) {
return(c(arg1, arg2))
}
just_return(2, 1)
[1] 2 1

Binary operators

Standard function syntax:

sum(c(1,2))
[1] 3

Operator syntax:

1 + 2
[1] 3

Most binary operators come in %:

3 %in% c(1,2,3)
[1] TRUE


Custom binary operators – let’s define an operator for “not in”:

`%!in%` <- function(x, y) !(x %in% y)
3 %!in% c(1,2,3)
[1] FALSE

Control structures – if

if a condition is true, do something.

if (1 + 1 == 2) print("True")
[1] "True"


add_or_subtract <- function(arg1, arg2, operation) {
 if (operation == "add") {
   result <- arg1 + arg2
 }
 if (operation == "subtract") {
   result <- arg1 + arg2
 }
 result
}
add_or_subtract(2,1,"add")
[1] 3

Control structures – else

else instructs what to do when the if condition is not met.

if (1 + 1 == 3) print("True") else print("False")
[1] "False"


add_or_subtract <- function(arg1, arg2, operation) {
 if (operation == "add") {
   result <- arg1 + arg2
 } else {
   result <- arg1 - arg2
 }
 return(result)
}
add_or_subtract(2,1,"subtract")
[1] 1

Control structures – switch

Instead of many if and else statements, try switch

fossil_description <- function(fossil) {
 switch(fossil,
  ammonite = "coiled shell",
  Tyrannosaurus = "serrated teeth",
  Lepidodendron = "scaly bark", 
  "not a fossil"
 )
}
fossil_description("Tyrannosaurus")
[1] "serrated teeth"
fossil_description("Lewis")
[1] "not a fossil"

Control structures – for loops

Loops are used for repeating similar actions multiple times. for loops iterate over a set of values. The iterator (i) changes with every iteration of the loop:

for(i in c(1,2,3)) print(i)
[1] 1
[1] 2
[1] 3

To generate sequences of integers, we can use seq_len. Let’s make a function:

print_repetitions <- function(n) {
 for (i in seq_len(n)) { 
   print(i)
 }
}
print_repetitions(2)
[1] 1
[1] 2

Control structures – while loops

while loops repeat a task until a condition is no longer met.

add_until_4 <- function(x) {
  while(x < 4) {
    x <- x + 1
    print(x)
  }
}
add_until_4(1)
[1] 2
[1] 3
[1] 4

Exercise 1 - Function for latitudinal binning

Create a function that can sort a data.frame into latitudinal bins. That is, we want a new column that identifies the bin of each entry of the data set. As an exemplary data set, we can use the reefs data from palaeoverse.

If you are new to writing R functions, try a simpler function that can sort data into the northern and southern hemisphere.

Here is what the result may look like when sorted into hemispheres:

reefs[72:73, c("name", "lat", "hemisphere")]
          name     lat hemisphere
72 Begunjscica 46.4333      north
73     W-Ceram -3.2500      south

Best practices

Naming style

Give variables and functions consistent names. These are the two most common styles:

snake_case

used by Tidyverse’s style guide

CamelCase

used by Google’s R style guide

Clean coding

Common practices:

  • Avoid renaming existing functions and variables
mean <- mean(c(1, 2, 3))
mean
[1] 2
mean(c(1, 2, 3))
[1] 2

R is clever, in this case this still works.

  • Use <- for assignment, not =
data = c(1, 2, 3)
data <- c(1, 2, 3)

Example of a detailed style guide: Tidyverse’s style guide

Comments

  • document purpose and usage of code
  • explain complex / non-intuitive code
# Calculate convex hull
tmp <- tmp[chull(x = tmp[, lng], 
                 y = tmp[, lat]), c(lng, lat)]
  • organise code into sections
#=== Set-up ===
  unique_taxa <- unique(occdf[, name])
  # Order taxa
  unique_taxa <- unique_taxa[order(unique_taxa)]
#=== convex hull  ===

Good code needs less comments - but comment enough that you can still use your code two years later.

Comments

Add some general information in the beginning of a large R script.

### Change point regression analysis
### July 2021
### Kilian Eichenseer
###
### Bayesian algorithm for finding a change point in 
### the linear relationship between two variables. 
### Uses JAGS (https://mcmc-jags.sourceforge.io/).

### Generate the data
set.seed(10) 
n <- 60 # number of total data points

… or use proper documentation

Documentation

?mean

roxygen2

  • R package, install with install.packages("roxygen2")
  • Used to create documentation for R packages (functions, data, …)
  • Start every line of documentation with ’#

This could generate documentation for the subtract function from earlier:

#' Subtraction
#' 
#' Subtracts `arg2` from `arg1`
#'
#' @param arg1 `Numeric`. First argument.
#' @param arg2 `Numeric`. Second argument.
#' @return A `numeric` containing the difference 
#' between `arg1` and `arg2`.
#' @examples
#' subtract(2,1)

?subtract

Exercise 2 - Document your function

blank

Funders

  • European Union’s Horizon 2020 research and innovation program (MAPAS project)
    • Grant number: 947921
  • The Royal Society
    • Grant numbers: RF_ERE_210013, RGF_R1_180020, and RGF_EA_180318
  • Juan de la Cierva-formación fellowship
    • FJC2020-044836-I / MCIN /AEI / 10.13039 /501100011033
  • ETH+ Research Grant (BECCY project)
  • FAPESP Postdoctoral fellowship
    • Grant number: 2022/05697-9
  • Population Biology Program of Excellence Postdoctoral Fellowship (the University of Nebraska-Lincoln)
  • Lerner-Gray Postdoctoral Research Fellowship (American Museum of Natural History)